Ruby3 がリリースされてめでたい!
RBS という型定義ファイルでコードチェックする機能がデフォルトで使えるようになった(実際は Steep という型チェックライブラリと合わせて使う)。
今後主要なリポジトリで RBS ファイルが提供されて、エディタサポートが効いたりすると生産性がめちゃ変わりそう。
ということでいくつか実験してみたいことがあったので、触ってみてる。
※ あくまでキャッチアップも兼ねた実験です
普段Railsアプリケーションを触っていて、クラッシュレポートで見かける No1 は NoMethodError。とりわけリクエストパラメータに関する nil アクセスが非常に多い所感があります。
何が来るか分からんということと、それらに対するバリデーション、特にネストされたパラメータになればなるほど nil アクセスによる NoMethodError が多くなる印象。
ActiveModel使ってリクエストパラメータを扱うフォームレイヤみたいなものを作ることも多いと思うが、ネストしてるとバリデーション書いていくの大変なんですよね・・
Java みたいな言語だとリクエストパラメータを POJO で定義してコントローラで型チェック済なオブジェクトを受け取れるおかげでだいぶ楽できる。
あれを Ruby でも出来ないかなー、つまりはランタイムで RBS の型定義を利用して定義と異なるパラメータが来たら落とすということが出来ないか実験してみた。
こんな感じでリクエストパラメータを RBS ファイルで定義したとする。
class UsersController
# ex: { name: "bob", age: 20, job: { title: "engineer", grade: 5 }, skill: ["ruby"] }
class CreateRequest < Struct[String | Integer | Job | Array[String]]
attr_reader name: String
attr_reader age: Integer
attr_reader job: Job
attr_reader skill: Array[String]
def initialize: (name: String, age: Integer, job: Job, skill: Array[String]) -> void
end
class Job < Struct[String | Integer]
attr_reader title: String
attr_reader grade: Integer
def initialize: (title: String, grade: Integer) -> void
end
end
RBS のリポジトリの README を読むと分かるが、RBS ファイルに定義したクラスの型情報を以下のコードで取ることができる。
loader = RBS::EnvironmentLoader.new
loader.add(path: signature_file_path)
rbs_env = RBS::Environment
.from_loader(loader)
.resolve_type_names
builder = RBS::DefinitionBuilder.new(env: rbs_env)
instance = builder.build_instance(
RBS::TypeName.new(
name: :CreateRequest,
namespace: RBS::Namespace.new(absolute: true, path: [:UsersController])
)
)
puts instance.instance_variables
=> UsersController::CreateRequest struct の attributes の型情報が取れる
これを少しごにょってみると、いい感じに struct の attributes とその型が何かを持ったハッシュを作ることができる。
kv_pairs = instance.instance_variables.map do |key, value|
k = key.to_s.delete("@").to_sym
name = value.type.name
[k, name.name]
end
kv_pairs.to_h
=> {:name=>:String, :age=>:Integer, :job=>{:title=>:String, :grade=>:Integer}, :skill=>:Array}
こいつとリクエストパラメータを比較することで、いい感じにチェックできるんじゃないかということで、gem にしてみた。
定義と異なる構造のパラメータがきたらこんな感じでエラーが取れる。
全然関係ないけど、初めての main ブランチ。。
rbs_gem をインストールできれば動くので、Ruby 2.6以上で動くはず。
typed_params というメソッドを通すと、RBS ファイルで型チェックして、こんな感じで ActionController で撃ち落とすことができそう。
class UsersController
include TypedParams
rescue_from TypeParams::InvalidTypeError, with: :request_error_handler
def index
index_params = typed_params(params, 'Users::IndexRequest')
response = some_resources(index_params)
render json: response status: :ok
end
private
def request_error_handler
render json: { error: 'invalid request' }, status: :bad_request
end
end
例えば、
- value の型が違う(String <-> Integer 等)
- ネストされた JSON が不適切
- key が不足している
- 想定外の key が渡ってきている
という場合に InvalidTypeError を発生させるので resque_from で拾えばよい。
最初ネストしたハッシュパラメータの場合どう型チェックできるかなとガチャガチャやっていて、Struct で定義すればネストしていてもチェックできるやんということに気づいた。
結構便利に使えそうな気はするが、意外と RBS ファイルの読み込みに時間がかかるので、気軽にやるのはまだ厳しい感じがする。
また、割と頻繁に変更されていきそうな RBS のクラスを結構触っているのでメンテも大変そう。
狙った RBS::Definition を取るまでが結構大変なので、良い感じの API が欲しくなってくる。